/*
 * Copyright 2024 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

import * as assert from 'assert';
import * as path from 'path';
import * as grpc from '../src';
import { TestClient, loadProtoFile } from './common';

const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto');
const echoService = loadProtoFile(protoFile)
  .EchoService as grpc.ServiceClientConstructor;

const AUTH_HEADER_KEY = 'auth';
const AUTH_HEADER_ALLOWED_VALUE = 'allowed';
const testAuthInterceptor: grpc.ServerInterceptor = (
  methodDescriptor,
  call
) => {
  const authListener = (new grpc.ServerListenerBuilder())
    .withOnReceiveMetadata((metadata, mdNext) => {
      if (
        metadata.get(AUTH_HEADER_KEY)?.[0] !== AUTH_HEADER_ALLOWED_VALUE
      ) {
        call.sendStatus({
          code: grpc.status.UNAUTHENTICATED,
          details: 'Auth metadata not correct',
        });
      } else {
        mdNext(metadata);
      }
    }).build();
  const responder = (new grpc.ResponderBuilder())
    .withStart(next => next(authListener)).build();
  return new grpc.ServerInterceptingCall(call, responder);
};

let eventCounts = {
  receiveMetadata: 0,
  receiveMessage: 0,
  receiveHalfClose: 0,
  sendMetadata: 0,
  sendMessage: 0,
  sendStatus: 0,
};

function resetEventCounts() {
  eventCounts = {
    receiveMetadata: 0,
    receiveMessage: 0,
    receiveHalfClose: 0,
    sendMetadata: 0,
    sendMessage: 0,
    sendStatus: 0,
  };
}

/**
 * Test interceptor to verify that interceptors see each expected event by
 * counting each kind of event.
 * @param methodDescription
 * @param call
 */
const testLoggingInterceptor: grpc.ServerInterceptor = (
  methodDescription,
  call
) => {
  return new grpc.ServerInterceptingCall(call, {
    start: next => {
      next({
        onReceiveMetadata: (metadata, mdNext) => {
          eventCounts.receiveMetadata += 1;
          mdNext(metadata);
        },
        onReceiveMessage: (message, messageNext) => {
          eventCounts.receiveMessage += 1;
          messageNext(message);
        },
        onReceiveHalfClose: hcNext => {
          eventCounts.receiveHalfClose += 1;
          hcNext();
        },
      });
    },
    sendMetadata: (metadata, mdNext) => {
      eventCounts.sendMetadata += 1;
      mdNext(metadata);
    },
    sendMessage: (message, messageNext) => {
      eventCounts.sendMessage += 1;
      messageNext(message);
    },
    sendStatus: (status, statusNext) => {
      eventCounts.sendStatus += 1;
      statusNext(status);
    },
  });
};

const testHeaderInjectionInterceptor: grpc.ServerInterceptor = (
  methodDescriptor,
  call
) => {
  return new grpc.ServerInterceptingCall(call, {
    start: next => {
      const authListener: grpc.ServerListener = {
        onReceiveMetadata: (metadata, mdNext) => {
          metadata.set('injected-header', 'present');
          mdNext(metadata);
        },
      };
      next(authListener);
    },
  });
};

const callExaminationInterceptor: grpc.ServerInterceptor = (
  methodDescriptor,
  call
) => {
  const connectionInfo = call.getConnectionInfo();
  return new grpc.ServerInterceptingCall(call, {
    sendMetadata: (metadata, next) => {
      metadata.add('local-address', `${connectionInfo.localAddress}`);
      metadata.add('local-port', `${connectionInfo.localPort}`);
      metadata.add('remote-address', `${connectionInfo.remoteAddress}`);
      metadata.add('remote-port', `${connectionInfo.remotePort}`);
      next(metadata);
    }
  })
}

describe('Server interceptors', () => {
  describe('Auth-type interceptor', () => {
    let server: grpc.Server;
    let client: TestClient;
    /* Tests that an interceptor can entirely prevent the handler from being
     * invoked, based on the contents of the metadata. */
    before(done => {
      server = new grpc.Server({ interceptors: [testAuthInterceptor] });
      server.addService(echoService.service, {
        echo: (
          call: grpc.ServerUnaryCall<any, any>,
          callback: grpc.sendUnaryData<any>
        ) => {
          // A test will fail if a request makes it to the handler without the correct auth header
          assert.strictEqual(
            call.metadata.get(AUTH_HEADER_KEY)?.[0],
            AUTH_HEADER_ALLOWED_VALUE
          );
          callback(null, call.request);
        },
      });
      server.bindAsync(
        'localhost:0',
        grpc.ServerCredentials.createInsecure(),
        (error, port) => {
          assert.ifError(error);
          client = new TestClient(`localhost:${port}`, false);
          done();
        }
      );
    });
    after(done => {
      client.close();
      server.tryShutdown(done);
    });
    it('Should accept a request with the expected header', done => {
      const requestMetadata = new grpc.Metadata();
      requestMetadata.set(AUTH_HEADER_KEY, AUTH_HEADER_ALLOWED_VALUE);
      client.sendRequestWithMetadata(requestMetadata, done);
    });
    it('Should reject a request without the expected header', done => {
      const requestMetadata = new grpc.Metadata();
      requestMetadata.set(AUTH_HEADER_KEY, 'not allowed');
      client.sendRequestWithMetadata(requestMetadata, error => {
        assert.strictEqual(error?.code, grpc.status.UNAUTHENTICATED);
        done();
      });
    });
  });
  describe('Logging-type interceptor', () => {
    let server: grpc.Server;
    let client: TestClient;
    before(done => {
      server = new grpc.Server({ interceptors: [testLoggingInterceptor] });
      server.addService(echoService.service, {
        echo: (
          call: grpc.ServerUnaryCall<any, any>,
          callback: grpc.sendUnaryData<any>
        ) => {
          callback(null, call.request);
        },
      });
      server.bindAsync(
        'localhost:0',
        grpc.ServerCredentials.createInsecure(),
        (error, port) => {
          assert.ifError(error);
          client = new TestClient(`localhost:${port}`, false);
          done();
        }
      );
    });
    after(done => {
      client.close();
      server.tryShutdown(done);
    });
    beforeEach(() => {
      resetEventCounts();
    });
    it('Should see every event once', done => {
      client.sendRequest(error => {
        assert.ifError(error);
        assert.deepStrictEqual(eventCounts, {
          receiveMetadata: 1,
          receiveMessage: 1,
          receiveHalfClose: 1,
          sendMetadata: 1,
          sendMessage: 1,
          sendStatus: 1,
        });
        done();
      });
    });
  });
  describe('Header injection interceptor', () => {
    let server: grpc.Server;
    let client: TestClient;
    before(done => {
      server = new grpc.Server({
        interceptors: [testHeaderInjectionInterceptor],
      });
      server.addService(echoService.service, {
        echo: (
          call: grpc.ServerUnaryCall<any, any>,
          callback: grpc.sendUnaryData<any>
        ) => {
          assert.strictEqual(
            call.metadata.get('injected-header')?.[0],
            'present'
          );
          callback(null, call.request);
        },
      });
      server.bindAsync(
        'localhost:0',
        grpc.ServerCredentials.createInsecure(),
        (error, port) => {
          assert.ifError(error);
          client = new TestClient(`localhost:${port}`, false);
          done();
        }
      );
    });
    after(done => {
      client.close();
      server.tryShutdown(done);
    });
    it('Should inject the header for the handler to see', done => {
      client.sendRequest(done);
    });
  });
  describe('Multiple interceptors', () => {
    let server: grpc.Server;
    let client: TestClient;
    before(done => {
      server = new grpc.Server({
        interceptors: [
          testAuthInterceptor,
          testLoggingInterceptor,
          testHeaderInjectionInterceptor,
        ],
      });
      server.addService(echoService.service, {
        echo: (
          call: grpc.ServerUnaryCall<any, any>,
          callback: grpc.sendUnaryData<any>
        ) => {
          assert.strictEqual(
            call.metadata.get(AUTH_HEADER_KEY)?.[0],
            AUTH_HEADER_ALLOWED_VALUE
          );
          assert.strictEqual(
            call.metadata.get('injected-header')?.[0],
            'present'
          );
          call.sendMetadata(new grpc.Metadata());
          callback(null, call.request);
        },
      });
      server.bindAsync(
        'localhost:0',
        grpc.ServerCredentials.createInsecure(),
        (error, port) => {
          assert.ifError(error);
          client = new TestClient(`localhost:${port}`, false);
          done();
        }
      );
    });
    after(done => {
      client.close();
      server.tryShutdown(done);
    });
    beforeEach(() => {
      resetEventCounts();
    });
    it('Should not log requests rejected by auth', done => {
      const requestMetadata = new grpc.Metadata();
      requestMetadata.set(AUTH_HEADER_KEY, 'not allowed');
      client.sendRequestWithMetadata(requestMetadata, error => {
        assert.strictEqual(error?.code, grpc.status.UNAUTHENTICATED);
        assert.deepStrictEqual(eventCounts, {
          receiveMetadata: 0,
          receiveMessage: 0,
          receiveHalfClose: 0,
          sendMetadata: 0,
          sendMessage: 0,
          sendStatus: 0,
        });
        done();
      });
    });
    it('Should log requests accepted by auth', done => {
      const requestMetadata = new grpc.Metadata();
      requestMetadata.set(AUTH_HEADER_KEY, AUTH_HEADER_ALLOWED_VALUE);
      client.sendRequestWithMetadata(requestMetadata, error => {
        assert.ifError(error);
        assert.deepStrictEqual(eventCounts, {
          receiveMetadata: 1,
          receiveMessage: 1,
          receiveHalfClose: 1,
          sendMetadata: 1,
          sendMessage: 1,
          sendStatus: 1,
        });
        done();
      });
    });
  });
  describe('Call properties', () => {
    let server: grpc.Server;
    let client: TestClient;
    let portNum: number;
    /* Tests that an interceptor can entirely prevent the handler from being
     * invoked, based on the contents of the metadata. */
    before(done => {
      server = new grpc.Server({ interceptors: [callExaminationInterceptor] });
      server.addService(echoService.service, {
        echo: (
          call: grpc.ServerUnaryCall<any, any>,
          callback: grpc.sendUnaryData<any>
        ) => {
          callback(null, call.request);
        },
      });
      server.bindAsync(
        '127.0.0.1:0',
        grpc.ServerCredentials.createInsecure(),
        (error, port) => {
          assert.ifError(error);
          client = new TestClient(`localhost:${port}`, false);
          portNum = port;
          done();
        }
      );
    });
    after(done => {
      client.close();
      server.tryShutdown(done);
    });
    it('Should get valid connection information', done => {
      const call = client.sendRequest(done);
      call.on('metadata', metadata => {
        assert.strictEqual(metadata.get('local-address')[0], '127.0.0.1');
        assert.strictEqual(metadata.get('remote-address')[0], '127.0.0.1');
        assert.strictEqual(metadata.get('local-port')[0], `${portNum}`);
        assert.notStrictEqual(metadata.get('remote-port')[0], 'undefined');
      });
    });
  });
});
